/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server.plugin.impl;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;

import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jp.fores.midori.core.util.ClassUtil;
import jp.fores.midori.server.WebContext;
import jp.fores.midori.server.config.MidoriConfigManager;
import jp.fores.midori.server.exception.IllegalServiceClassException;
import jp.fores.midori.server.exception.MidoriServerException;
import jp.fores.midori.server.exception.RPCIllegalArgumentException;
import jp.fores.midori.server.exception.RPCTargetMethodNotFoundException;
import jp.fores.midori.server.exception.RPCTargetServiceNotFoundException;
import jp.fores.midori.server.plugin.RPCPlugin;
import jp.fores.midori.server.plugin.RPCPluginUtil;
import jp.fores.midori.server.plugin.RPCTargetMethodManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import flex.messaging.MessageException;
import flex.messaging.io.MessageDeserializer;
import flex.messaging.io.MessageIOConstants;
import flex.messaging.io.MessageSerializer;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.TypeMarshallingContext;
import flex.messaging.io.amf.ActionContext;
import flex.messaging.io.amf.ActionMessage;
import flex.messaging.io.amf.AmfMessageDeserializer;
import flex.messaging.io.amf.AmfMessageSerializer;
import flex.messaging.io.amf.AmfTrace;
import flex.messaging.io.amf.MessageBody;
import flex.messaging.messages.AcknowledgeMessage;
import flex.messaging.messages.CommandMessage;
import flex.messaging.messages.ErrorMessage;
import flex.messaging.messages.Message;
import flex.messaging.messages.RemotingMessage;

/**
 * AMF形式のリクエスト・レスポンスの処理を行うプラグインクラス。<br>
 */
//(処理を全てBlazeDSのライブラリに丸投げするため、AbstractRPCPluginを継承するのではなく、
// より自由度の高いRPCPluginインターフェースを直接実装する)
public class AmfPlugin implements RPCPlugin {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static Log log = LogFactory.getLog(AmfPlugin.class);

	/**
	 * Content-Type
	 */
	public static final String CONTENT_TYPE = "application/x-amf";


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void handleRequest(WebContext context) throws Exception {
		//==========================================================
		//リクエストヘッダのチェック

		//リクエストの「content-type」ヘッダの値を取得
		String contentType = context.request.getContentType();

		//content-typeが定数の値と一致しない場合
		if (!CONTENT_TYPE.equals(contentType)) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("不正なcontent-typeです。:" + contentType);
			}

			//BAD_REQUEST(400)のレスポンスエラーを返す
			context.response.sendError(HttpServletResponse.SC_BAD_REQUEST);

			//以降の処理を行わない
			return;
		}


		//==========================================================
		//メインの処理

		//ActionContextクラスのインスタンスを生成
		ActionContext actionContext = new ActionContext();

		try {
			//リクエストメッセージの処理を行う
			processRequestMessage(context.request, actionContext);

			//レスポンスメッセージを作成する
			createResponseMessage(actionContext);

			//レスポンスメッセージを出力する
			outputResponseMessage(context.response, actionContext);
		}
		//例外処理
		catch (Throwable e) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("AMFの処理に失敗しました。", e);
			}

			//サービス呼び出しに失敗した場合の例外処理を行う
			handleError(e, actionContext);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void init(ServletConfig config) throws Exception {
		//TypeMarshallingContextのクラスローダーを、現在のスレッドのクラスローダーを使うように差し替える
		TypeMarshallingContext.setTypeMarshallingContext(new TypeMarshallingContext() {
			@Override
			public void setClassLoader(ClassLoader loader) {
				super.setClassLoader(Thread.currentThread().getContextClassLoader());
			}
		});
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void destroy() throws Exception {
		//特に何も行わない
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * リクエストメッセージの処理を行います。<br>
	 *
	 * @param request リクエスト
	 * @param actionContext ActionContext
	 * @throws Exception 例外
	 */
	protected void processRequestMessage(HttpServletRequest request,
			ActionContext actionContext) throws Exception {
		//AMFメッセージのデシリアライズ用のクラスのインスタンスを生成
		MessageDeserializer deserializer = new AmfMessageDeserializer();

		//デシリアライズ用のクラスを、リクエストの入力ストリームで初期化する
		deserializer.initialize(SerializationContext.getSerializationContext(), request.getInputStream(), new AmfTrace());

		//ActionContextのデシリアライズ対象のバイト数に、リクエストの「Content-Length」ヘッダの値を設定する
		actionContext.setDeserializedBytes(request.getContentLength());

		//ActionContextのリクエストメッセージに新たなインスタンスを設定する
		actionContext.setRequestMessage(new ActionMessage());

		//デシリアライズ用のクラスを使って、リクエストメッセージの読み込みを行う
		deserializer.readMessage(actionContext.getRequestMessage(), actionContext);
	}

	/**
	 * レスポンスメッセージを作成します。<br>
	 *
	 * @param actionContext ActionContext
	 * @throws Exception 例外
	 */
	protected void createResponseMessage(ActionContext actionContext)
			throws Exception {
		//ActionContextのリクエストメッセージボディーを取得
		MessageBody requestMessageBody = actionContext.getRequestMessageBody();

		//レスポンスメッセージボディーのインスタンスを生成
		MessageBody responseMessageBody = new MessageBody();

		//レスポンスメッセージボディーのターゲットURIに、リクエストメッセージボディーから取得したレスポンス用のURIを設定する
		responseMessageBody.setTargetURI(requestMessageBody.getResponseURI());

		//リクエストメッセージに含まれているデータを取得
		Object requestData = requestMessageBody.getData();

		//リクエストメッセージに含まれているデータの型がListの場合
		if (requestData instanceof List) {
			//Listから配列に変換する
			requestData = ((List) requestData).toArray(new Object[0]);
		}

		//リクエストメッセージに含まれているデータの型が配列の場合
		if (requestData.getClass().isArray()) {
			//リクエストメッセージに含まれているデータを元の型(配列)に戻す
			Object[] requestDataArray = (Object[]) requestData;

			//配列の全ての要素に対して処理を行う
			for (Object requestMessage : requestDataArray) {
				//現在の要素に対して、レスポンスメッセージボディーの処理を行う
				processResponseMessageBody(responseMessageBody, requestMessage);
			}
		}
		//それ以外の場合
		else {
			//レスポンスメッセージボディーの処理を行う
			processResponseMessageBody(responseMessageBody, requestData);
		}

		//レスポンスメッセージのインスタンスを生成
		ActionMessage responseMessage = new ActionMessage();

		//レスポンスメッセージにActionContextから取得したバージョンを設定する
		responseMessage.setVersion(actionContext.getVersion());

		//レスポンスメッセージに先ほど作成したレスポンスメッセージボディーを追加する
		responseMessage.addBody(responseMessageBody);

		//ActionContextにレスポンスメッセージを設定する
		actionContext.setResponseMessage(responseMessage);
	}

	/**
	 * レスポンスメッセージボディーの処理を行います。<br>
	 *
	 * @param responseMessageBody レスポンスメッセージボディー
	 * @param requestMessage リクエストメッセージ
	 * @throws Exception 例外
	 */
	protected void processResponseMessageBody(MessageBody responseMessageBody,
			Object requestMessage) throws Exception {
		//リクエストメッセージの型がCommandMessageの場合
		if (requestMessage instanceof CommandMessage) {
			//リクエストメッセージを元の型にキャストする
			CommandMessage command = (CommandMessage) requestMessage;

			//PING用のオペレーションの場合
			//(FlashPlyerからのAMF呼び出しは、最初にPING用のリクエストが投げられて、2回目に本物のリクエストが投げられるらしいので、
			// PING用のリクエストは無視する必要がある)
			if (command.getOperation() == CommandMessage.CLIENT_PING_OPERATION) {
				//レスポンスメッセージボディーの返答方法に、結果用の値を設定
				//(PINGの応答用なので、設定する必要があるのはこれのみで、レスポンスデータなどは設定しない)
				responseMessageBody.setReplyMethod(MessageIOConstants.RESULT_METHOD);
			}
		}
		//リクエストメッセージの型がRemotingMessageの場合
		else if (requestMessage instanceof RemotingMessage) {
			//リクエストメッセージを元の型にキャストする
			RemotingMessage remoting = (RemotingMessage) requestMessage;

			//リクエストメッセージのdestinationを取得
			String destination = remoting.getDestination();

			//リクエストメッセージのoperationを取得
			String operation = remoting.getOperation();

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("リクエストメッセージのdestination:" + destination
						+ ", operation:" + operation);
			}


			//==========================================================
			//サービスのメソッドの呼び出し

			//destinationを元にして、呼び出し対象のサービスクラスの完全指定のクラス名を取得
			String className = getTargetClassName(destination);

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("呼び出し対象のクラス名:" + className);
			}

			//クラス名が取得できなかった場合
			if ((className == null) || "".equals(className)) {
				//RPCの対象のサービスクラスが見つからなかった場合の例外を投げる
				throw new RPCTargetServiceNotFoundException("呼び出し対象のクラス名を取得できませんでした。");
			}

			//コンテナから呼び出し対象のサービスクラスのインスタンスを取得
			Object service = RPCPluginUtil.getService(WebContext.getInstance().container, className);

			//operationをメソッド名として使用する
			String methodName = operation;

			//引数の配列
			//(初期値はから配列)
			Object[] args = new Object[0];

			//リクエストメッセージの引数のListを取得
			List<Object> argsList = remoting.getParameters();

			//引数のListがnullでない場合
			if (argsList != null) {
				//引数のListを配列に変換する
				args = argsList.toArray(new Object[0]);
			}

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("呼び出し対象のメソッド名:" + methodName);
			}

			//サービスクラスのインスタンスから、RPCの入力データを格納するためのDTOの内容に適合するRPC対象のメソッドを取得
			Method method = findRPCTargetMethod(service, methodName, args);

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("呼び出し対象のメソッド:" + method.toGenericString());
			}

			//呼び出し対象のメソッドにあわせて引数の補正を行う
			adjustMethodArgs(method, args);

			//メソッドの戻り値
			Object result = null;

			try {
				////////////////////////////////////////////////////////////
				//TODO:サービスクラスのinit()的なメソッドをここで呼び出す?
				////////////////////////////////////////////////////////////

				//RPC対象のメソッドを呼び出して、戻り値を取得する
				result = method.invoke(service, args);

				////////////////////////////////////////////////////////////
				//TODO:サービスクラスのdestroy()的なメソッドをここで呼び出す?
				////////////////////////////////////////////////////////////

			}
			//引数の型が違うためにメソッド呼び出しに失敗した場合
			catch (IllegalArgumentException e) {
				//捕捉した例外を、RPCの引数の形式が不正な場合に発生する例外でラッピングして投げ直す
				throw new RPCIllegalArgumentException("引数の型が違うためにメソッド呼び出しに失敗しました。", e);
			}


			//==========================================================
			//メソッドの戻り値を元にして、レスポンスメッセージボディーを設定する

			//レスポンスメッセージボディーの返答方法に、結果用の値を設定
			responseMessageBody.setReplyMethod(MessageIOConstants.RESULT_METHOD);

			//結果用のメッセージのインスタンスを生成
			AcknowledgeMessage resultMessage = new AcknowledgeMessage();

			//リクエストメッセージの情報を結果用のメッセージに設定する
			resultMessage.setCorrelationId(remoting.getMessageId());
			resultMessage.setDestination(remoting.getDestination());
			resultMessage.setClientId(remoting.getClientId());

			//結果用のメッセージにメソッドの戻り値を設定
			resultMessage.setBody(result);

			//結果用のメッセージのバージョン用のヘッダに「3.0」を設定
			resultMessage.setHeader(CommandMessage.MESSAGING_VERSION, new Double(3.0));

			//レスポンスメッセージボディーのデータに結果用のメッセージを設定
			responseMessageBody.setData(resultMessage);
		}
	}

	/**
	 * レスポンスメッセージを出力します。<br>
	 *
	 * @param response レスポンス
	 * @param actionContext ActionContext
	 * @throws Exception 例外
	 */
	protected void outputResponseMessage(HttpServletResponse response,
			ActionContext actionContext) throws Exception {
		//AMFメッセージのシリアライズ用のクラスのインスタンスを生成
		MessageSerializer serializer = new AmfMessageSerializer();

		//シリアライズ用のクラスを、レスポンスの出力ストリームで初期化する
		serializer.initialize(SerializationContext.getSerializationContext(), response.getOutputStream(), new AmfTrace());

		//シリアライズ用のクラスを使って、リクエストメッセージの書き込みを行う
		serializer.writeMessage(actionContext.getResponseMessage());
	}

	/**
	 * サービス呼び出しに失敗した場合の例外処理を行います。<br>
	 *
	 * @param e 捕捉した例外
	 * @param actionContext ActionContext
	 * @throws Exception 例外
	 */
	protected void handleError(Throwable e, ActionContext actionContext)
			throws Exception {
		//==========================================================
		//リクエストメッセージを取得

		//ActionContextのリクエストメッセージボディーに含まれているデータを取得
		Object requestData = actionContext.getRequestMessageBody().getData();

		//リクエストメッセージ
		Message requestMessage = null;

		//リクエストメッセージに含まれているデータが取得できた場合
		if (requestData != null) {
			//リクエストメッセージに含まれているデータの型がListの場合
			if (requestData instanceof List) {
				//Listの最初のデータをリクエストメッセージとして取得する
				requestMessage = (Message) ((List) requestData).get(0);
			}
			//リクエストメッセージに含まれているデータの型が配列の場合
			else if (requestData.getClass().isArray()) {
				//配列の最初のデータをリクエストメッセージとして取得する
				requestMessage = (Message) Array.get(requestData, 0);
			}
		}

		//リクエストメッセージが取得できなかった場合
		if (requestMessage == null) {
			//どうしようもないので、例外を投げる
			throw new MidoriServerException("リクエストメッセージが取得できませんでした。");
		}


		//==========================================================
		//例外のメッセージ文字列の取得

		//捕捉した例外が動的に呼び出したメソッドで発生する例外の場合
		if (e instanceof InvocationTargetException) {
			//対象の例外を捕捉した例外の発生元に切り替える
			//(InvocationTargetException自体には価値がないため)
			e = e.getCause();
		}

		//捕捉した例外のメッセージ文字列を取得
		String errorStr = e.getMessage();

		//例外のメッセージ文字列を取得が取得できなかった場合
		if (errorStr == null) {
			//捕捉した例外クラスのクラス名をメッセージ文字列の代わりに使用する
			errorStr = e.getClass().getName();
		}


		//==========================================================
		//ErrorMessageの作成

		//MessageExceptionのインスタンスを生成
		MessageException messageException = new MessageException();

		//MessageExceptionに先ほど取得した例外のメッセージ文字列を設定する
		//(「10000」という数字が何を意味しているのかは不明だが、そのままにしておく)
		messageException.setMessage(10000, new Object[] { errorStr });

		//MessageExceptionからErrorMessageのインスタンスを生成
		ErrorMessage errorMessage = messageException.createErrorMessage();

		//リクエストメッセージの情報をErrorMessageに設定する
		errorMessage.setCorrelationId(requestMessage.getMessageId());
		errorMessage.setDestination(requestMessage.getDestination());
		errorMessage.setClientId(requestMessage.getClientId());


		//==========================================================
		//ErrorMessageを元にして、レスポンスメッセージを作成

		//ActionContextのステータスにエラーを設定
		actionContext.setStatus(MessageIOConstants.STATUS_ERR);

		//レスポンスメッセージボディーのインスタンスを生成
		MessageBody responseMessageBody = new MessageBody();

		//レスポンスメッセージボディーの返答方法に、ステータス用の値を設定
		responseMessageBody.setReplyMethod(MessageIOConstants.STATUS_METHOD);

		//レスポンスメッセージボディーのデータに先ほど作成したErrorMessageを設定
		responseMessageBody.setData(errorMessage);

		//レスポンスメッセージのインスタンスを生成
		ActionMessage responseMessage = new ActionMessage();

		//レスポンスメッセージに先ほど作成したレスポンスメッセージボディーを追加する
		responseMessage.addBody(responseMessageBody);

		//ActionContextにレスポンスメッセージを設定する
		actionContext.setResponseMessage(responseMessage);


		//==========================================================
		//レスポンスメッセージを出力
		outputResponseMessage(WebContext.getInstance().response, actionContext);
	}

	/**
	 * destinationを元にして、呼び出し対象のサービスクラスの完全指定のクラス名を取得します。<br>
	 * 取得できなかった場合は、nullを返します。<br>
	 *
	 * @param destination destination
	 * @return クラスの完全指定の名前
	 * @throws Exception 例外
	 */
	protected String getTargetClassName(String destination) throws Exception {
		//destinationがnullまたは空文字列の場合
		if ((destination == null) || "".equals(destination)) {
			//クラス名を特定できないので、nullを返す
			return null;
		}


		//==========================================================
		//設定内容のルートパッケージとdestinationから、呼び出し対象のサービスクラスの完全指定のクラス名を作成する

		//作業用のStringBuilderのインスタンスを生成
		StringBuilder sb = new StringBuilder();


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//ルートパッケージ

		//設定内容のルートパッケージを取得
		String rootPackage = MidoriConfigManager.getInstance().getConfig().rootPackage;

		//ルートパッケージがnull、または空文字列でない場合
		if ((rootPackage != null) && !"".equals(rootPackage)) {
			//ルートパッケージをStringBuilderに追加する
			sb.append(rootPackage);

			//次の要素との区切りための「.」をStringBuilderに追加する
			sb.append(".");
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//サービスパッケージ

		//サービスパッケージをStringBuilderに追加する
		//(ここの部分は固定)
		sb.append("service");

		//次の要素との区切りための「.」をStringBuilderに追加する
		sb.append(".");


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//クラス名

		//destinationをそのままクラス名として使用する
		String className = destination;

		//クラス名の最初の文字を大文字にする
		className = Character.toUpperCase(className.charAt(0))
				+ className.substring(1);

		//クラス名をStringBuilderに追加する
		sb.append(className);


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//結果を文字列に変換して返す
		return sb.toString();
	}

	/**
	 * サービスクラスのインスタンスから、メソッド名と引数に適合するRPC対象のメソッドを取得します。<br>
	 * 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合や、適合するメソッドが存在しない場合は例外を投げます。<br>
	 *
	 * @param service サービスクラスのインスタンス
	 * @param methodName メソッド名
	 * @param args 引数の配列
	 * @return 適合するメソッド
	 * @throws IllegalServiceClassException 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合
	 * @throws RPCTargetMethodNotFoundException 適合するメソッドが存在しない場合
	 * @throws Exception 例外
	 */
	protected Method findRPCTargetMethod(Object service, String methodName,
			Object[] args) throws IllegalServiceClassException,
			RPCTargetMethodNotFoundException, Exception {
		//RPCの対象のメソッド情報を管理するクラスに処理を移譲する
		return RPCTargetMethodManager.getInstance().findRPCTargetMethod(service, methodName, args);
	}

	/**
	 * 呼び出し対象のメソッドにあわせて引数の補正を行います。<br>
	 *
	 * @param method 呼び出し対象のメソッド
	 * @param args 引数の配列
	 * @throws Exception 例外
	 */
	protected void adjustMethodArgs(Method method, Object[] args)
			throws Exception {
		//メソッドの引数の型の配列を取得
		Class[] classes = method.getParameterTypes();

		//メソッドの引数の型の配列の要素数と、引数の配列の要素数が一致しない場合
		//(事前にこのチェックは行われているはずだが、念のためにここでもチェック)
		if (classes.length != args.length) {
			//RPCの対象のメソッドが見つからなかった場合の例外を投げる
			throw new RPCTargetMethodNotFoundException("引数の数が一致しません。");
		}

		//引数が存在しない場合
		if (args.length == 0) {
			//以降の処理を行わない
			return;
		}

		//メソッドの引数のTypeオブジェクトの配列を取得
		Type[] types = method.getGenericParameterTypes();

		//引数の配列の要素数に応じてループをまわす
		for (int i = 0; i < args.length; i++) {
			//現在の引数のオブジェクトを、メソッドの引数の型に補正する
			args[i] = ClassUtil.adjustObjectType(args[i], classes[i], types[i]);
		}
	}

}
